// Copyright (c) 2018-2019,  Jiang Si. For more information see 'LICENSE'


#include "GSApp.h"

#include "Window/WindowGLFW.h"
#include "GSStl/Export.h"
#include "GSStl/Algorithms/StringParser.h"
#include "GSStl/Math/Vec.h"
#include <thread>

#include "Tools/GSLibraryLoad.h"
#include "Meleon/PipelineCompiler/EShaderCompilationFlags.h"

#ifdef FG_ENABLE_LODEPNG
#	include "lodepng/lodepng.h"
#endif


namespace GS
{
	namespace {
		static constexpr uint	UpdateAllReferenceDumps = false;
	}

	/*
	=================================================
		constructor
	=================================================
	*/
	GSApp::GSApp()
	{
		_pMeleon = null;
	}

	/*
	=================================================
		destructor
	=================================================
	*/
	GSApp::~GSApp()
	{
	}

	/*
	=================================================
		OnResize
	=================================================
	*/
	void GSApp::OnResize(const uint2 &size)
	{
		if (Any(size == uint2(0)))
			return;

		VulkanSwapchainCreateInfo	swapchain_info;
		swapchain_info.surface = BitCast<SurfaceVk_t>(_pMeleon->GetVkSurface());
		swapchain_info.surfaceSize = size;

		_swapchainId = _frameGraph->CreateSwapchain(swapchain_info, _swapchainId.Release());
		CHECK_FATAL(_swapchainId);
	}

	void GSApp::OnUpdate()
	{
		_MainLoop();
		_frameID++;
	}

	/*
	=================================================
		_Initialize
	=================================================
	*/
	bool GSApp::_Initialize(WindowPtr &&wnd)
	{
		//	load Meleon
		CGSLibrary meleonLibrary(GSLibraryDefName("Meleon"));
		if (meleonLibrary.IsLoaded())
		{
			PFN_CreateMeleon CreateMeleonInterface = meleonLibrary.GetProcedureAddress<PFN_CreateMeleon>("CreateMeleon");
			if (CreateMeleonInterface != nullptr)
			{
				_pMeleon = CreateMeleonInterface();
			}

			meleonLibrary.ReleaseOwnership();
		}
		CHECK_ERR(_pMeleon);

		const uint2		wnd_size{ 800, 600 };

		// initialize window
		{
			_window = std::move(wnd);
			CHECK_ERR(_window->Create(wnd_size, "Test"));
			_window->AddListener(this);
		}

		// create vulkan device
		{
			CHECK_ERR(_pMeleon->CreateVulkan(_window->GetVulkanSurface(), "Test", "FrameGraph", VK_API_VERSION_1_1,
				"",
				{ { VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_SPARSE_BINDING_BIT | VK_QUEUE_PRESENT_BIT, 0.0f },
				 { VK_QUEUE_COMPUTE_BIT,  0.0f },
				 { VK_QUEUE_TRANSFER_BIT, 0.0f } },
				_pMeleon->GetRecomendedInstanceLayers(),
				_pMeleon->GetRecomendedInstanceExtensions(),
				_pMeleon->GetAllDeviceExtensions()
			));

			// this is a test and the test should fail for any validation error
			_pMeleon->CreateDebugUtilsCallback(DebugUtilsMessageSeverity_All,
				[](const VulkanDeviceExt::DebugReport &rep) { CHECK_FATAL(not rep.isError); });
		}

		// setup device info
		VulkanDeviceInfo					vulkan_info;
		IFrameGraph::SwapchainCreateInfo_t	swapchain_info;
		{
			vulkan_info.instance = BitCast<InstanceVk_t>(_pMeleon->GetVkInstance());
			vulkan_info.physicalDevice = BitCast<PhysicalDeviceVk_t>(_pMeleon->GetVkPhysicalDevice());
			vulkan_info.device = BitCast<DeviceVk_t>(_pMeleon->GetVkDevice());

			VulkanSwapchainCreateInfo	swapchain_ci;
			swapchain_ci.surface = BitCast<SurfaceVk_t>(_pMeleon->GetVkSurface());
			swapchain_ci.surfaceSize = _window->GetSize();
			swapchain_info = swapchain_ci;

			for (auto& q : _pMeleon->GetVkQueues())
			{
				VulkanDeviceInfo::QueueInfo	qi;
				qi.handle = BitCast<QueueVk_t>(q.handle);
				qi.familyFlags = BitCast<QueueFlagsVk_t>(q.flags);
				qi.familyIndex = q.familyIndex;
				qi.priority = q.priority;
				qi.debugName = "";

				vulkan_info.queues.push_back(qi);
			}
		}

		// initialize framegraph
		{
			_frameGraph = _pMeleon->CreateFrameGraph(vulkan_info);
			CHECK_ERR(_frameGraph);

			_swapchainId = _frameGraph->CreateSwapchain(swapchain_info, Default, "Window");
			CHECK_ERR(_swapchainId)
		}
		// add glsl pipeline compiler
		{
			_pplnCompiler = _pMeleon->CreatePipelineCompiler(vulkan_info.physicalDevice, vulkan_info.device);
			_pplnCompiler->SetCompilationFlags(GS::EShaderCompilationFlags::AutoMapLocations |
				GS::EShaderCompilationFlags::Quiet |
				GS::EShaderCompilationFlags::ParseAnnoations |
				GS::EShaderCompilationFlags::UseCurrentDeviceLimits);

			_frameGraph->AddPipelineCompiler(_pplnCompiler);
		}
		_CreateGraphicPipeline();
		_pMeleon->SetSubmitCmdBufferNums(2);
		return true;
	}

	bool GSApp::_CreateGraphicPipeline()
	{
		HGraphicPipeline GraphicPipeline=0;
		_pMeleon->BeginGraphicsPipelineDesc(GraphicPipeline);


		//// add vertex shader
		//// VKSL_100 means that it is GLSL with vulkan semantics and should be compiled for Vulkan 1.0.
		//// SPIRV_110 means that it is SPIRV binary compiled for Vulkan 1.1.
		//// VkShader_100 used for already created shader module.
		//// GLSL_450 means that shader compatible with OpenGL 4.5, but also can be used in Vulkan.
		//// Warning: any SPIRV and VKSL shaders have higher priority than any GLSL shader if used Vulkan backend (currently it is only one backend).
		_pMeleon->AddShader(GraphicPipeline,EShader::Vertex, EShaderLangFormat::VKSL_100, "main", R"#(
						out vec3  v_Color;

						const vec2  g_Positions[3] = vec2[](
							vec2(0.0, -0.5),
							vec2(0.5, 0.5),
							vec2(-0.5, 0.5)
						);

						const vec3  g_Colors[3] = vec3[](
							vec3(1.0, 0.0, 0.0),
							vec3(0.0, 1.0, 0.0),
							vec3(0.0, 0.0, 1.0)
						);

						void main() {
							gl_Position = vec4( g_Positions[gl_VertexIndex], 0.0, 1.0 );
							v_Color = g_Colors[gl_VertexIndex];
						}
						)#");

		//// add fragment shader
		_pMeleon->AddShader(GraphicPipeline,EShader::Fragment, EShaderLangFormat::VKSL_100, "main", R"#(
						in  vec3  v_Color;

						layout(location=0) out vec4  out_Color;

						void main() {
							out_Color = vec4(v_Color, 1.0);
						}
						)#");

		// create pipeline from description.
		_gPipelineId=_pMeleon->EndGraphicsPipelineDesc(GraphicPipeline);
		return true;
	}

	bool GSApp::_MainLoop()
	{
		// keep submitted command buffers
		//_pMeleon->SetSubmitCmdBufferNums(2);
		//for (uint frame_id = 0;; ++frame_id)
		//{
			// returned command buffer will be in the recording state
			_pMeleon->FrameGraphBegin(EQueueType::Graphics);

			// create render target
			const uint2 view_size = _window->GetSize();
			//const uint2		view_size = _frameGraph->GetDescription(image).dimension.xy();
			//RawImageID image = _pMeleon->GetSwapchainImage(cmdBuff,_swapchainId,GS::ESwapchainImage::Primary);
			
			ImageResDesc  image_desc{ EImage::Tex2D,
								   uint3{view_size, 1},
								   EPixelFormat::RGBA8_UNorm,
								   EImageUsage::ColorAttachment };

			ImageID  image = _pMeleon->CreateImage(image_desc);

			HRenderPass renderPass=0;
			
			// set render area size
			_pMeleon->BeginRenderPass(renderPass, view_size);

			// add render target.
			// render target ID must have same index as output parameter in fragment shader.
			// before rendering image will be cleared with 0.0f value.
			// after rendering result will be stored to the image.
			_pMeleon->AddTarget(renderPass, RenderTargetID{ 0 }, image, RGBA32f{ 0.0f }, EAttachmentStoreOp::Store);

			// setup viewport
			_pMeleon->AddViewport(renderPass, view_size);

			// setup render states
			_pMeleon->SetDepthTestEnabled(renderPass, false);

			// create render pass
			LogicalPassID  render_pass = _pMeleon->EndRenderPass(renderPass);

			// create draw task
			DrawVertices  draw_triangle;

			// draw 3 vertices
			draw_triangle.Draw(3);

			// draw as triangle list
			draw_triangle.SetTopology(EPrimitive::TriangleList);

			// use pipeline from previous chapter
			draw_triangle.SetPipeline(_gPipelineId);

			// add draw task to the render pass
			_pMeleon->GetCommandBufferOp()->AddTask(render_pass, draw_triangle);

			// add render pass to the frame graph.
			// after that you can not add draw tasks into the render pass.
			Task submit = _pMeleon->GetCommandBufferOp()->AddTask(SubmitRenderPass{ render_pass });

			// present to swapchain.
			// this task must be executed after drawing.
			Task present = _pMeleon->GetCommandBufferOp()->AddTask(Present{ _swapchainId, image }.DependsOn(submit));

			// finilize recording, compile frame graph for command buffer.
			// command buffer may be submitted at any time.
			_pMeleon->FrameGraphExecute();

			// submit all pending command buffers and present all pending swapchain images.
			_pMeleon->FrameGraphEnd();

			DeleteResources(image);
		//}
	
		return false;
	}

	/*
	=================================================
		_Update
	=================================================
	*/
	bool GSApp::_Update()
	{
		if (not _window->Update())
			return false;

		//if (Any(_window->GetSize() == uint2(0)))
		//{
		//	std::this_thread::sleep_for(std::chrono::duration< float >{ 0.01f });	// ~100 fps
		//	return true;
		//}

		//// wait frame-2 for double buffering
		//_frameGraph->Wait({ _submittedBuffers[_frameId] });
		//_submittedBuffers[_frameId] = null;

		//if (not DrawScene())
		//	return true;

		//++_frameId;

		//CHECK_ERR(_frameGraph->Flush());

		//_UpdateFrameStat();
		return true;
	}

	/*
	=================================================
		_Destroy
	=================================================
	*/
	void GSApp::_Destroy()
	{
		_pplnCompiler = null;

		_frameGraph->ReleaseResource(_gPipelineId);
		_frameGraph->ReleaseResource(_swapchainId);

		_pMeleon->Destroy();
		if (_window)
		{
			_window->Destroy();
			_window.reset();
		}
	}

	/*
	=================================================
		SavePNG
	=================================================
	*/
#ifdef FG_ENABLE_LODEPNG
	bool GSApp::SavePNG(const String &filename, const ImageView &imageData) const
	{
		LodePNGColorType	colortype = LodePNGColorType(-1);
		uint				bitdepth = 0;
		uint				bitperpixel = 0;

		switch (imageData.Format())
		{
		case EPixelFormat::RGBA8_UNorm:
		case EPixelFormat::BGRA8_UNorm:
			colortype = LodePNGColorType::LCT_RGBA;
			bitdepth = 8;
			bitperpixel = 4 * 8;
			break;

		case EPixelFormat::RGBA16_UNorm:
			colortype = LodePNGColorType::LCT_RGBA;
			bitdepth = 16;
			bitperpixel = 4 * 16;
			break;

		case EPixelFormat::RGB8_UNorm:
		case EPixelFormat::BGR8_UNorm:
			colortype = LodePNGColorType::LCT_RGB;
			bitdepth = 8;
			bitperpixel = 3 * 8;
			break;

		case EPixelFormat::RGB16_UNorm:
			colortype = LodePNGColorType::LCT_RGB;
			bitdepth = 16;
			bitperpixel = 3 * 16;
			break;

		case EPixelFormat::R8_UNorm:
			colortype = LodePNGColorType::LCT_GREY;
			bitdepth = 8;
			bitperpixel = 8;
			break;

		case EPixelFormat::R16_UNorm:
			colortype = LodePNGColorType::LCT_GREY;
			bitdepth = 16;
			bitperpixel = 16;
			break;

		default:
			RETURN_ERR("unsupported pixel format!");
		}


		uint	err = 0;

		if (imageData.Parts().size() == 1 and imageData.RowPitch() == imageData.RowSize())
		{
			err = lodepng::encode(filename, imageData.data(), imageData.Dimension().x, imageData.Dimension().y, colortype, bitdepth);
		}
		else
		{
			const size_t	row_size = size_t(imageData.RowSize());
			Array<uint8_t>	pixels;		pixels.resize(row_size * imageData.Dimension().y);

			for (uint y = 0; y < imageData.Dimension().y; ++y)
			{
				auto	row = imageData.GetRow(y);

				::memcpy(pixels.data() + (row_size * y), row.data(), row_size);
			}

			err = lodepng::encode(filename, pixels.data(), imageData.Dimension().x, imageData.Dimension().y, colortype, bitdepth);
		}

		CHECK_ERR(err == 0);
		//const char * error_text = lodepng_error_text( err );

		return true;
	}
#endif	// FG_ENABLE_LODEPNG

	/*
	=================================================
		Visualize
	=================================================
	*/
	bool GSApp::Visualize(StringView name) const
	{
#	if defined(FG_GRAPHVIZ_DOT_EXECUTABLE) and defined(FG_STD_FILESYSTEM)

		String	str;
		CHECK_ERR(_frameGraph->DumpToGraphViz(OUT str));

		auto	path = std::filesystem::path{ FG_TEST_GRAPHS_DIR }.append(name.data()).replace_extension("dot");

		CHECK(GraphViz::Visualize(str, path, "png", false, true));

#	else
		// not supported
#	endif
		return true;
	}

	/*
	=================================================
		CompareDumps
	=================================================
	*/
	bool GSApp::CompareDumps(StringView filename) const
	{
		String	fname{ FG_TEST_DUMPS_DIR };	fname << '/' << filename << ".txt";

		String	right;
		CHECK_ERR(_frameGraph->DumpToString(OUT right));

		// override dump
		if (UpdateAllReferenceDumps)
		{
			GS_STL::WriteFile(fname, right);
			return true;
		}

		// read from file
		String	left;
		{
			GS_STL::ReadFile(fname, OUT left);
		}

		size_t		l_pos = 0;
		size_t		r_pos = 0;
		uint2		line_number;
		StringView	line_str[2];

		const auto	LeftValid = [&l_pos, &left]() { return l_pos < left.length(); };
		const auto	RightValid = [&r_pos, &right]() { return r_pos < right.length(); };

		const auto	IsEmptyLine = [](StringView str)
		{
			for (auto& c : str) {
				if (c != '\n' and c != '\r' and c != ' ' and c != '\t')
					return false;
			}
			return true;
		};


		// compare line by line
		for (; LeftValid() and RightValid(); )
		{
			// read left line
			do {
				StringParser::ReadLineToEnd(left, INOUT l_pos, OUT line_str[0]);
				++line_number[0];
			} while (IsEmptyLine(line_str[0]) and LeftValid());

			// read right line
			do {
				StringParser::ReadLineToEnd(right, INOUT r_pos, OUT line_str[1]);
				++line_number[1];
			} while (IsEmptyLine(line_str[1]) and RightValid());

			if (line_str[0] != line_str[1])
			{
				RETURN_ERR("in: "s << filename << "\n\n"
					<< "line mismatch:" << "\n(" << ToString(line_number[0]) << "): " << line_str[0]
					<< "\n(" << ToString(line_number[1]) << "): " << line_str[1]);
			}
		}

		if (LeftValid() != RightValid())
		{
			RETURN_ERR("in: "s << filename << "\n\n" << "sizes of dumps are not equal!");
		}

		return true;
	}

	/*
	=================================================
		CreateData
	=================================================
	*/
	Array<uint8_t>	GSApp::CreateData(BytesU size) const
	{
		Array<uint8_t>	arr;
		arr.resize(size_t(size));

		return arr;
	}

	/*
	=================================================
		Run
	=================================================
	*/
	void GSApp::Run()
	{
		GSApp				app;
		UniquePtr<IWindow>	wnd;

#if defined( FG_ENABLE_GLFW )
		wnd.reset(new WindowGLFW());

#elif defined( FG_ENABLE_SDL2 )
		wnd.reset(new WindowSDL2());

#elif defined(FG_ENABLE_SFML)
		wnd.reset(new WindowSFML());

#else
#	error Unknown window library!
#endif

		CHECK_FATAL(app._Initialize(std::move(wnd)));

		for (; app._Update(); ) {}

		app._Destroy();
	}


}	// GS
